
import arrow
import bson
import json

from urllib import parse
import datetime
from typing import List

from bson import ObjectId
from flask import g

from app.db.models.assets import TaskSettings, CustomTaskSettings
from app.db.models.balances import Balances
from app.db.models.tasks import Task, AlertSettings
from app.db.models.users import User
from app.db.models.asset_tasks import (
    HostVulSettings,
    WsTaskTime as DbWsTaskTime,
    ScanSpeedConfig as DB_ScanSpeedConfig
)
from app.db.models.jobs import (
    HttpSettings,
    Job,
    PingSettings,
    SecurityEventSettings,
    SslSettings,
    CustomSslSettings,
    CustomSecurityEventSettings,
    VulSettings,
    ChangeCheckSettings,
    CustomVulSettings,
    SpecialVulSettings,
    ContentSettings,
    CustomContentSettings,
    PreLogin,
    CustomMonitorPage as Db_CustomMonitorPage
)
from pydantic import Field, BaseModel, validator
from app.db.models.asset_tasks import AssetTasks
from wtforms.validators import DataRequired, Length, URL
from wtforms.fields import (
    StringField, BooleanField,
    FieldList, FormField,
    MultipleFileField, IntegerField
)
from wtforms import Form

from app.libs.regexp import URL_SUFFIX_RE, NAME_RE, KEYWORD_RE
from app.validators import BaseForm, UidForm
from app.libs.ip import is_ipv4
from app.libs.utility import compared_value, trans_textbox_to_dict
from app.handler.v2_balances import BalancesOne
from app.errors import (
    JobNotVerified, PackageExpired,
    ParameterError, CustomNameError
)
from app.libs.enums import (
    VulType,
    TaskType
)
from app.libs.enums import ScanSpeed


class AddJobForm(BaseForm):
    note = StringField(label='资产名称', validators=[DataRequired()])
    targetUrl = StringField(label='资产地址', validators=[DataRequired(), URL()])
    sourceIp = StringField('源站IP', default='')
    enableMonitor = BooleanField('开启监测', default=False)
    enableAlert = BooleanField('开启告警', default=False)
    immediateExec = BooleanField('立即执行', default=False)
    uid = StringField('用户ID', validators=[DataRequired()])

    def validate_note(self, field):
        field.data = field.data.strip()
        if not NAME_RE.fullmatch(field.data):
            raise CustomNameError()

    def validate_sourceIp(self, field):
        flag, msg = is_ipv4(field.data)
        if not flag:
            raise ParameterError(msg=msg)


class JobBatchVerification(BaseForm):
    job_ids = FieldList(StringField(validators=[DataRequired(), Length(min=24, max=24)]), min_entries=1)
    is_verified = BooleanField()
    note = StringField(validators=[Length(max=51)])
    enable_task = BooleanField(default=False)


class JobCydEnableForm(BaseForm):
    uid = StringField(validators=[DataRequired(), Length(min=24, max=24)])
    job_id = StringField(validators=[DataRequired(), Length(min=24, max=24)])


class Addition(Form):
    pass


class JobForm(BaseForm):
    jobId = StringField(validators=[DataRequired()], label='Jobs._id')
    addition = FormField(Addition)
    ignore_check = BooleanField(default=False)
    prelogin = MultipleFileField(default={})

    def __init__(self, data=None):
        self.job = None
        self.asset = None
        self.balances = None
        self.ignore_check = False
        super().__init__(data=data)
        self._validate_jobId(self.data.get('jobId'))

    def validate_ignore_check(self, value):
        if g.role.name in ['普通用户']:
            return False

        return value.data

    def _validate_jobId(self, value):
        try:
            value = ObjectId(value)
        except Exception:
            raise ParameterError(msg='jobId format error')
        if g.entity_type == 'user':
            self.job = Job.objects.get_or_404(pk=value, uid=g.user.pk)
        else:
            self.job = Job.objects.get_or_404(pk=value)

        if self.job.assetType != 'host':
            self.asset = self.job.get_asset()
        self.balances = Balances.objects.filter(uid=self.job.uid).first()

        if not self.job.verification.isVerified and not self.ignore_check:
            raise JobNotVerified()

    def validate_prelogin(self, value):
        try:
            trans_textbox_to_dict(value.data.get('request_http_headers', ""))
        except Exception:
            raise ParameterError(msg="headers 格式不正确")
        data = PreLogin.from_dict(value.data)
        data.validate()
        self.prelogin = data

    def _validate_settings_common(self, setting):
        # Skip when disabled.
        enable_monitor = getattr(self, 'enableMonitor', None)
        if (enable_monitor is not None and not enable_monitor.data) or not getattr(setting, 'enable', True):
            return

        if self.ignore_check.data:
            return

        # check feature
        if not getattr(self.balances.feature, f"{self.task_type}Task", False):
            setting.enable = False
            return

        # check expire time.
        if self.balances.planExpireTime < datetime.datetime.utcnow():
            raise PackageExpired()
        # check feq
        if 'enable' in setting._fields and setting.enable and (freq := setting.interval):
            self._validate_num(f"{self.task_type}MaxFreq", freq, 'lt')

        # check maxPage
        if max_page := getattr(setting.collect, 'maxPage', None):
            self._validate_num(f"{self.task_type}MaxPage", max_page, 'gt')

        # check node
        if max_node := len(set(getattr(setting.collect, 'area', []) + getattr(setting.collect, 'area_ipv6', []))):
            self._validate_num(f"{self.task_type}MaxNode", max_node, 'gt')

        # check depth
        if max_depth := getattr(setting.collect, 'depth', None):
            self._validate_num(f"{self.task_type}MaxDepth", max_depth, 'gt')

    def _validate_num(self, key, data, _type='gt'):
        # gte: data >= balance return false

        if gte_value := getattr(self.balances.balance, key, None):
            gte_value = gte_value.total

            if not compared_value(data, gte_value, _type):
                if key.endswith('MaxFreq'):
                    raise ParameterError(msg="监测频率小于套餐开通的监测频率")
                else:
                    raise ParameterError(msg=f"{key} 超过套餐限制")


class JobNoCheckBalancesForm(JobForm):
    def _validate_jobId(self, value):
        try:
            value = ObjectId(value)
        except Exception:
            raise ParameterError(msg='jobId format error')
        if g.entity_type == 'user':
            self.job = Job.objects.get_or_404(pk=value, uid=g.user.pk)
        else:
            self.job = Job.objects.get_or_404(pk=value)
        if self.job.assetType != 'host':
            self.asset = self.job.get_asset()


class ScanSpeedConfig(Form):
    scanSpeed = IntegerField(default=ScanSpeed.AUTO.value)
    concurrency = IntegerField(default=0)

    def validate_scanSpeed(self, field):
        if field.data and field.data not in [scan_speed.value for scan_speed in ScanSpeed.__members__.values()]:
            raise ParameterError(msg='扫描速度配置错误')

    def validate_concurrency(self, field):
        if field.data:
            if field.data < 0:
                raise ParameterError(msg='并发数配置错误')
            if field.data > 300:
                raise ParameterError(msg='并发量不能超过300')


class CustomMonitorPage(Form):
    includeUrl = FieldList(StringField(), default=[])
    excludeUrl = FieldList(StringField(), default=[])

    @staticmethod
    def check_path_url(suffix_url, msg):
        if not isinstance(suffix_url, str):
            raise ParameterError(msg=msg)
        suffix_url = suffix_url.strip()
        if not URL_SUFFIX_RE.match(suffix_url):
            raise ParameterError(msg=msg)
        return suffix_url

    @staticmethod
    def check_full_url(url, msg):
        if not isinstance(url, str):
            raise ParameterError(msg=msg)
        full_url = url.strip()
        try:
            obj = parse.urlparse(url=full_url)
        except:
            raise ParameterError(msg=msg)
        if not (all([obj.scheme, obj.netloc, obj.path]) and URL_SUFFIX_RE.match(obj.path)):
            raise ParameterError(msg=msg)
        return full_url

    def validate_includeUrl(self, field):
        """校验额外爬取的url地址格式正确"""

        exists = set()
        for suffix_url in field.data:
            exists.add(self.check_path_url(suffix_url, "需额外监测的页面地址格式错误"))
        if len(exists) > 100:
            raise ParameterError(msg="需额外监测的页面地址最多添加100个参数")

    def validate_excludeUrl(self, field):
        exists = set()
        for keyword_url in field.data:
            exists.add(self.check_path_url(keyword_url, "不需监测的页面地址格式错误"))
        if len(exists) > 100:
            raise ParameterError(msg="不需监测的页面地址最多添加100个参数")


class SettingsForm(JobForm):
    sourceIp = StringField(label="源ip地址")
    enableMonitor = BooleanField('全局启用', default=True)
    immediateExec = BooleanField('立即执行', default=False)
    customMonitorPage = FormField(CustomMonitorPage)

    def __init__(self, data=None):
        self.settings = None
        super().__init__(data=data)

    def is_changed(self):
        return True

    def has_task_not_schedule(self):
        """是否有task没有下发, 没有taskId就是没有下发的.

        Returns:
            [type]: [description]
        """
        return not self.settings.taskId

    @staticmethod
    def mongo_data_check(setting):
        try:
            setting.validate()
        except:
            raise ParameterError(msg="参数错误")
        if hasattr(setting, "name"):
            name = setting.name
            if name and not NAME_RE.fullmatch(name):
                raise CustomNameError()
        return setting


class HttpSettingsForm(SettingsForm):
    task_type = TaskType.http.value
    httpSettings = FieldList(StringField(), default=[])

    def validate_httpSettings(self, value):
        settings = []
        job = self.job if getattr(self, "job", "") else Job.objects.filter(id=self.jobId.data).first()
        for setting in value.data:
            target = setting.get("target", "")
            url_obj = parse.urlparse(target)
            if job.targetUrl != f"{url_obj.scheme}://{url_obj.netloc}":
                raise ParameterError(msg=f"监测目标错误{target}, 不是一个已经存在的资产")
            if not url_obj.path:
                raise ParameterError(msg=f"监测目标格式错误{target}, http监测必须填写路径")
            setting = HttpSettings.from_dict(setting)
            if len(setting.collect.area) + len(setting.collect.area_ipv6) < setting.alert.nodeCount:
                raise ParameterError(msg="告警策略中告警节点数不可超过配置的监测节点数")
            setting = self.mongo_data_check(setting)
            self._validate_settings_common(setting)
            settings.append(setting)
        self.settings = settings

    def has_task_not_schedule(self):
        for setting in self.job.httpSettings:
            if not setting.taskId:
                return True
        return False


class PingSettingsForm(SettingsForm):
    task_type = TaskType.ping.value
    pingSettings = FieldList(StringField(), default=[])

    def validate_pingSettings(self, value):
        settings = []
        job = self.job if getattr(self, "job", "") else Job.objects.filter(id=self.jobId.data).first()
        for setting in value.data:
            target = setting.get("target", "")
            url_obj = parse.urlparse(target)
            if job.targetUrl != f"{url_obj.scheme}://{url_obj.netloc}":
                raise ParameterError(msg=f"监测目标错误{target}, 不是一个已经存在的资产")
            setting = PingSettings.from_dict(setting)
            if len(setting.collect.area) + len(setting.collect.area_ipv6) < setting.alert.nodeCount:
                raise ParameterError(msg="告警策略中告警节点数不可超过配置的监测节点数")
            setting = self.mongo_data_check(setting)
            self._validate_settings_common(setting)
            settings.append(setting)
        self.settings = settings

    def has_task_not_schedule(self):
        for setting in self.job.pingSettings:
            if not setting.taskId:
                return True
        return False


class SecurityEventSettingsForm(SettingsForm):
    task_type = TaskType.security_event.value
    securityEventSettings = StringField(default={})

    def validate_securityEventSettings(self, value):
        setting = SecurityEventSettings.from_dict(value.data)
        setting = self.mongo_data_check(setting)
        self._validate_settings_common(setting)
        self.settings = setting

    def has_task_not_schedule(self):
        return not self.job.securityEventSettings.taskId


class ContentSettingsForm(SettingsForm):
    task_type = TaskType.content.value
    contentSettings = StringField(default={})

    def validate_contentSettings(self, value):
        setting = ContentSettings.from_dict(value.data)
        # 如果传入了指定的uri, 则加上targetUrl
        if urls := setting.collect.includeUrl:
            if len(urls) > 50:
                raise ParameterError(msg='超过指定url条数限制')
        setting.collect.customId = str(self.job.uid)
        setting = self.mongo_data_check(setting)
        self._validate_settings_common(setting)
        self.settings = setting

    def has_task_not_schedule(self):
        return not self.job.contentSettings.taskId


class VulSettingsForm(SettingsForm):
    task_type = TaskType.vul.value
    vulSettings = StringField(validators=[DataRequired()])

    def validate_vulSettings(self, value):
        setting = VulSettings.from_dict(value.data)
        setting = self.mongo_data_check(setting)
        self._validate_settings_common(setting)
        self.settings = setting

    def has_task_not_schedule(self):
        return not self.job.vulSettings.taskId


class SslSettingForm(SettingsForm):
    task_type = TaskType.ssl.value
    sslSettings = StringField(default={})

    def validate_sslSettings(self, value):
        setting = SslSettings.from_dict(value.data)
        setting = self.mongo_data_check(setting)
        self._validate_settings_common(setting)
        self.settings = setting

    def has_task_not_schedule(self):
        return not self.job.sslSettings.taskId


class ChangeCheckSettingForm(SettingsForm):

    task_type = TaskType.change_check.value
    changeCheckSettings = StringField(default={})

    def check_base_chang_count(self, setting):
        gte_value = getattr(self.balances.balance, "changeCheckMaxBaseChange", None)
        if setting.collect.baseChangeCount < getattr(gte_value, "total", 5) and setting.collect.enableBaseChange:
            raise ParameterError("变更监测自动变更样本频率小于套餐最小值")

    def check_keywords(self, setting):
        gte_value = getattr(self.balances.balance, "changeCheckMaxKeyword", None)
        if len(setting.collect.keywords) > gte_value.total:
            raise ParameterError("自定义违规词数量超过套餐限制")
        keywords = []
        for kw in setting.collect.keywords:
            if (not isinstance(kw, str)):
                raise ParameterError(f"自定义违规词 '{kw}' 格式错误")
            elif not (0 < len(kw.strip()) < 40):
                raise ParameterError(f"自定义违规词 '{kw}' 格式错误, 不能超过40个字符")
            keywords.append(kw.strip())
        setting.collect.keywords = keywords

    def check_resources(self, setting):
        gte_value = getattr(self.balances.balance, "changeCheckMaxResource", None)
        if len(setting.collect.resources) > gte_value.total:
            raise ParameterError("重点关注资源数量超过套餐限制")
        resources = []
        for resource in setting.collect.resources:
            resources.append(CustomMonitorPage.check_full_url(resource, msg=f"重点关注资源 {resource} 链接格式错误"))
        setting.collect.resources = resources

    def check_incloude_urls(self, setting):
        gte_value = getattr(self.balances.balance, "changeCheckMaxPage", None)
        if len(setting.collect.includeUrl) > gte_value.total:
            raise ParameterError("指定监测页面超过了套餐最大监测页面数")
        urls = []
        for url in setting.collect.includeUrl:
            urls.append(CustomMonitorPage.check_path_url(url, msg=f"指定监测页面 {url} 链接格式错误"))
        setting.collect.includeUrl = urls

    def check_excloude_urls(self, setting):
        # 暂时没有该功能
        setting.collect.excludeUrl = []

    def validate_changeCheckSettings(self, value):
        setting = ChangeCheckSettings.from_dict(value.data)
        setting = self.mongo_data_check(setting)
        self._validate_settings_common(setting)
        self.settings = setting
        self.check_base_chang_count(setting)
        self.check_keywords(setting)
        self.check_resources(setting)
        self.check_excloude_urls(setting)
        self.check_incloude_urls(setting)

    def has_task_not_schedule(self):
        return not self.job.changeCheckSettings.taskId


class AssetSettingForm(SettingsForm):
    task_type = TaskType.asset.value
    taskSettings = StringField(default={})

    def validate_taskSettings(self, value):
        setting = TaskSettings.from_dict(value.data)
        setting = self.mongo_data_check(setting)
        self._validate_settings_common(setting)
        self.settings = setting

    def has_task_not_schedule(self):
        return not self.asset.taskSettings.taskId

    def _validate_settings_common(self, setting):
        return


class HostVulSettingForm(SettingsForm):
    task_type = TaskType.hostVul.value
    asset_task_id = StringField(validators=[DataRequired()])
    hostVulSettings = StringField(validators=[DataRequired()])

    def validate_hostVulSettings(self, value):
        setting = HostVulSettings.from_dict(value.data)
        setting = self.mongo_data_check(setting)
        self._validate_settings_common(setting)
        self.settings = setting

    def validate_asset_task_id(self, value):
        try:
            value = ObjectId(value.data)
        except Exception:
            raise ParameterError(msg='asset task id format error')
        if g.entity_type == 'user':
            self.asset_task = AssetTasks.objects.get_or_404(pk=value, uid=g.user.pk)
        else:
            self.asset_task = AssetTasks.objects.get_or_404(pk=value)

    def has_task_not_schedule(self):
        return not self.asset_task.hostVulSettings.taskId


class CustomAddition(Addition):
    pk = StringField(validators=[Length(max=50)])

    def validate_pk(self, field):
        if not field.data:
            return
        try:
            data = ObjectId(field.data)
        except Exception:
            raise ParameterError(msg='taskId format error')
        if g.entity_type == 'user':
            Task.objects.get_or_404(pk=data, uid=g.user.pk)
        else:
            Task.objects.get_or_404(pk=data)


class CustomSettingsForm(JobForm):
    immediateExec = BooleanField('立即执行', default=True)
    taskSettings = StringField(default={})
    addition = FormField(CustomAddition)
    alertSettings = StringField(default={})
    # cookie = StringField(label='预登录cookie', default='')
    customMonitorPage = FormField(CustomMonitorPage)

    def __init__(self, data=None):
        self.job = None
        self.settings = None
        self.alert_setting = None
        super().__init__(data=data)

    def validate_taskSettings(self, value):
        pass

    def validate_alertSettings(self, field):
        alert_setting = AlertSettings.from_dict(field.data)
        alert_setting.smsDailyLimit = 99999
        alert_setting.emailDailyLimit = 99999
        alert_setting.validate()
        self.alert_setting = alert_setting


class CustomVulSettingsForm(CustomSettingsForm):
    task_type = TaskType.vul.value

    def validate_taskSettings(self, value):
        setting = CustomVulSettings.from_dict(value.data)
        setting.collect.type = VulType.full.value
        setting = SettingsForm.mongo_data_check(setting)
        self._validate_settings_common(setting)
        self.settings = setting


class SpecialVulSettingsForm(CustomSettingsForm):
    task_type = TaskType.vul.value

    def validate_taskSettings(self, value):
        setting = SpecialVulSettings.from_dict(value.data)
        setting = SettingsForm.mongo_data_check(setting)
        self._validate_settings_common(setting)
        self.settings = setting


class CustomSecurityEventSettingsForm(CustomSettingsForm):
    task_type = TaskType.security_event.value

    def validate_taskSettings(self, value):
        setting = CustomSecurityEventSettings.from_dict(value.data)
        setting = SettingsForm.mongo_data_check(setting)
        self._validate_settings_common(setting)
        self.settings = setting


class CustomContentSettingsForm(CustomSettingsForm):
    task_type = TaskType.content.value

    def validate_taskSettings(self, value):
        setting = CustomContentSettings.from_dict(value.data)
        setting.collect.customId = str(self.job.uid)
        setting = SettingsForm.mongo_data_check(setting)
        self._validate_settings_common(setting)
        self.settings = setting


class CustomSslSettingForm(CustomSettingsForm):
    task_type = TaskType.ssl.value

    def validate_taskSettings(self, value):
        if self.job.target.protocol == 'http:':
            raise ParameterError(msg="Unexpected target protocol 'http:'")
        setting = CustomSslSettings.from_dict(value.data)
        setting = SettingsForm.mongo_data_check(setting)
        self._validate_settings_common(setting)
        self.settings = setting


class CustomAssetSettingForm(CustomSettingsForm):
    task_type = TaskType.asset.value

    def validate_taskSettings(self, value):
        setting = CustomTaskSettings.from_dict(value.data)
        setting = SettingsForm.mongo_data_check(setting)
        self._validate_settings_common(setting)
        self.settings = setting


class JobsForm(BaseForm):
    jobIds = MultipleFileField(validators=[DataRequired()])
    addition = FormField(CustomAddition)
    ignore_check = BooleanField(default=False)
    prelogin = MultipleFileField(default={})

    def __init__(self, data=None):
        self.job_list = []
        self.asset_list = []
        super().__init__(data=data)
        self._validate_jobIds(self.data.get('jobIds'))

    def validate_ignore_check(self, value):
        if g.role.name in ['普通用户']:
            return False

        return value.data

    def _validate_jobIds(self, value):
        uid = None
        for item in value:
            try:
                item = ObjectId(item)
            except Exception:
                raise ParameterError(msg='jobId format error')
            if g.entity_type == 'user':
                job = Job.objects.get_or_404(pk=item, uid=g.user.pk)
            else:
                job = Job.objects.get_or_404(pk=item)
            self.job_list.append(job)
            self.asset_list.append(job.get_asset())

            if not job.verification.isVerified and not self.ignore_check:
                raise JobNotVerified()

            if not uid:
                uid = job.uid
        self.balances = Balances.objects.filter(uid=uid).first()

    def validate_prelogin(self, value):
        try:
            trans_textbox_to_dict(value.data.get('request_http_headers', ""))
        except Exception:
            raise ParameterError(msg="headers 格式不正确")
        data = PreLogin.from_dict(value.data)
        data.validate()
        self.prelogin = data

    def _validate_settings_common(self, setting):
        # Skip when disabled.
        enable_monitor = getattr(self, 'enableMonitor', None)
        if (enable_monitor is not None and not enable_monitor.data) or not getattr(setting, 'enable', True):
            return

        if self.ignore_check.data:
            return

        # check feature
        if not getattr(self.balances.feature, f"{self.task_type}Task", False):
            setting.enable = False
            return

        # check expire time.
        if self.balances.planExpireTime < datetime.datetime.utcnow():
            raise PackageExpired()
        # check feq
        if 'enable' in setting._fields and setting.enable and (freq := setting.interval):
            self._validate_num(f"{self.task_type}MaxFreq", freq, 'lt')

        # check maxPage
        if max_page := getattr(setting.collect, 'maxPage', None):
            self._validate_num(f"{self.task_type}MaxPage", max_page, 'gt')

        # check node
        if max_node := len(set(getattr(setting.collect, 'area', []) + getattr(setting.collect, 'area_ipv6', []))):
            self._validate_num(f"{self.task_type}MaxNode", max_node, 'gt')

        # check depth
        if max_depth := getattr(setting.collect, 'depth', None):
            self._validate_num(f"{self.task_type}MaxDepth", max_depth, 'gt')

    def _validate_num(self, key, data, _type='gt'):
        # gte: data >= balance return false

        if gte_value := getattr(self.balances.balance, key, None):
            gte_value = gte_value.total

            if not compared_value(data, gte_value, _type):
                raise ParameterError(msg=f"{key} 超过套餐限制")


class CustomIpv6SettingForm(JobsForm):
    task_type = TaskType.ipv6.value
    name = StringField(validators=[DataRequired(), Length(max=50)])
    asset_task_id = StringField()

    def validate_name(self, value):
        value.data = value.data.strip()
        if not NAME_RE.fullmatch(value.data):
            raise CustomNameError()


class AssetTaskForm(BaseForm):
    name = StringField(validators=[DataRequired()], label='AssetTasks.name')
    jobId = StringField(validators=[DataRequired()], label='AssetTasks.jobId')
    uid = StringField(label='AssetTasks.uid')
    enableMonitor = BooleanField(default=True)
    isVerified = BooleanField(default=True)
    taskSettings = StringField(default={})
    taskGroupType = StringField(default='ipv6')
    triggerType = StringField(default='schedule')
    taskType = StringField()

    def __init__(self, data):
        self.settings = None
        self.asset = None
        super(AssetTaskForm, self).__init__(data)
        self._init_job(self.data.get('jobId'))

    def _init_job(self, value):
        try:
            value = ObjectId(value)
        except Exception:
            raise ParameterError(msg='jobId format error')
        if g.entity_type == 'user':
            self.job = Job.objects.get_or_404(pk=value, uid=g.user.pk)
        else:
            self.job = Job.objects.get_or_404(pk=value)
        self.asset = self.job.get_asset()

        if not self.job.verification.isVerified:
            raise JobNotVerified()

    def validate_taskSettings(self, value):
        setting = CustomTaskSettings.from_dict(value.data)
        setting = SettingsForm.mongo_data_check(setting)
        # self._validate_settings_common(setting)
        self.settings = setting

    def validate_uid(self, value):
        if not value.data:
            return
        try:
            self.uid = ObjectId(value.data)
        except Exception:
            raise ParameterError(msg='uid format error')

    def validate_name(self, value):
        value.data = value.data.strip()
        if not NAME_RE.fullmatch(value.data):
            raise CustomNameError()

    def create(self):
        exclude_keys = ('taskSettings', )
        if not self.validate():
            raise ParameterError(msg=self.errors)

        validated_data = {k: v for k, v in self.data.items() if k not in exclude_keys}
        if g.role.name == '普通用户':
            validated_data['uid'] = g.user.id
        return AssetTasks(**validated_data).save()


class CustomAssetTaskForm(AssetTaskForm):
    def __init__(self, *args, **kwargs):
        self.balance_one = None
        super(CustomAssetTaskForm, self).__init__(*args, **kwargs)

    def _init_job(self, value):
        getattr(super(CustomAssetTaskForm, self), '_init_job')(value)
        self.balance_one = BalancesOne(uid=self.job.uid)


class CustomIPv6AssetTaskForm(CustomAssetTaskForm):
    taskType = StringField(default=TaskType.ipv6.value)
    triggerType = StringField(default='manual')


class BatchRemoveTaskForm(JobNoCheckBalancesForm):
    taskIds = FieldList(StringField(validators=[Length(max=50)]))


class BatchRemoveTaskSessionForm(JobNoCheckBalancesForm):
    taskSessionIds = FieldList(StringField(
        validators=[DataRequired()]), min_entries=1)


class BatchRemoveScannerTaskForm(BaseForm):
    taskIds = FieldList(StringField(validators=[Length(max=50)]))


class BatchStopTaskForm(JobNoCheckBalancesForm):
    taskIds = FieldList(StringField(validators=[Length(max=50)]))


class BatchStopTaskSessionForm(JobNoCheckBalancesForm):
    taskSessionIds = FieldList(StringField(
        validators=[DataRequired()]), min_entries=1)


class RescanTaskForm(JobForm):
    taskId = StringField(validators=[DataRequired()])


class JobDownloadForm(BaseForm):
    filter = StringField()

    def validate_filter(self, value):
        try:
            query_dict = dict()
            if not value.data:
                return query_dict

            value.data = json.loads(value.data)
            params = value.data
            for k, v in params.items():
                if k in ('verification', 'OR', 'note', 'target', 'enableMonitor', 'enableAlert', 'uidIn', 'assetType'):
                    if k == 'OR':
                        for item in v:
                            for key in item:
                                if key not in ('uid',):
                                    raise ParameterError(msg='filter value error')
                                item[key] = ObjectId(item[key])
                        query_dict['$or'] = v
                    elif k == 'target':
                        query_dict['target.hostname'] = v.get('hostname')
                    elif k == 'verification':
                        if params['verification']:
                            query_dict['verification.isVerified'] = v.get('isVerified')
                    elif k == 'uidIn':
                        _temp = []
                        for uid in v:
                            _temp.append(ObjectId(uid))
                        query_dict['uid'] = {'$in': _temp}
                    else:
                        query_dict[k] = v
            if 'assetType' not in query_dict:
                query_dict['assetType'] = {"$ne": "host"}
            value.data = query_dict
        except Exception as e:
            raise ParameterError(msg='filter value error')


class JobBatchMonitorOrAlertSwitchForm(BaseForm):
    switch = BooleanField()
    job_ids = FieldList(StringField(validators=[DataRequired()]), min_entries=1)
    user_ids = FieldList(StringField(validators=[DataRequired()]), min_entries=1)

    def validate_job_ids(self, value):
        try:
            job_ids = []
            if not value.data:
                raise ParameterError(msg='请选择要批量操作的资产')
            for job_id in value.data:
                job_ids.append(ObjectId(job_id))
            self.job_ids = job_ids
        except Exception as e:
            raise ParameterError(msg='job_ids value error')

    def validate_user_ids(self, value):
        try:
            user_ids = []
            if not value.data:
                raise ParameterError(msg='请选择要批量操作的资产')
            for user_id in value.data:
                user_ids.append(ObjectId(user_id))
            self.user_ids = user_ids
        except Exception as e:
            raise ParameterError(msg='user_ids value error')


class TaskSummary(BaseModel):
    task_group_type: str = Field(validators=[DataRequired()], name='任务分组类型', choices=['web', 'host', 'availability'])


class JobCount(BaseModel):
    uid: str = Field(None, name='用户名', min_length=0, max_length=50)
    task_group_type: str = Field(validators=[DataRequired()], name='资产类型', choices=['web', 'host', 'availability'])

    @validator('uid')
    def validate_uid(cls, v):
        if v:
            return ObjectId(v)
        return ''


class JobIdForm(BaseForm):
    uid = StringField(label="用户ID")
    trigger_type = StringField(label="任务监测类型", validators=[DataRequired()])
    asset_type = StringField(label="资产类型", validators=[DataRequired()])
    task_group_type = StringField(label="任务组类型", validators=[DataRequired()])
    exists = StringField(label="否是下发", default="false")

    def validate_uid(self, value):
        if g.role.name != '管理员':
            self.uid = g.audit_uid
            return self.uid
        if not value.data:
            raise ParameterError(msg='用户信息错误')
        try:
            self.uid = ObjectId(value.data)
        except bson.errors.BSONError:
            raise ParameterError(msg='用户信息错误')

    def validate_trigger_type(self, value):
        if value.data not in ['schedule', 'manual']:
            raise ParameterError(msg='任务类型选择错误')

    def validate_asset_type(self, value):
        if value.data not in ['web', 'host']:
            raise ParameterError(msg='资产类型选择错误')

    def validate_task_group_type(self, value):
        if value.data not in ['host', 'web_security', 'http', 'ping', 'asset']:
            raise ParameterError(msg='任务组类型选择错误')

    def validate_for_api(self):
        obj = super(JobIdForm, self).validate_for_api()
        if obj.asset_type.data == "host" and obj.task_group_type.data != "host":
            raise ParameterError(msg='主机资产只支持查询主机监测任务')
        if obj.asset_type.data == "web" and obj.task_group_type.data not in ['http', 'ping', 'web_asset', 'web_security']:
            raise ParameterError(msg='web资产只支持查询web监测任务')
        return obj


class HostVulTaskList(BaseModel):
    page: int = Field(1, name='页码')
    count: int = Field(10, name='条数', description='每页最大展示条数')

    task_group_type: str = Field(validators=[DataRequired()], name='任务分组类型',
                                 choices=['host', 'http', 'ping', 'web_asset', 'web_security'])
    trigger_type: str = Field(None, name='任务类型', choices=['schedule', 'manual'])
    target_url: str = Field(None, name='资产地址')
    level: str = Field(None, name='安全等级')
    status: str = Field(None, name='检测状态')


class AdminHostVulTaskList(BaseModel):
    page: int = Field(1, name='页码')
    perPage: int = Field(10, name='条数', description='每页最大展示条数')

    username: str = Field(None, name='用户名')
    task_group_type: str = Field(validators=[DataRequired()], name='任务分组类型',
                                 choices=['host', 'http', 'ping', 'web_asset', 'web_security'])
    target_url: str = Field(None, name='资产地址')
    status: str = Field(None, name='任务状态')
    trigger_type: str = Field(None, name='任务类型')


class HostVulTaskSettings(BaseModel):
    uid: str = Field(None, name='用户名', min_length=0, max_length=50)
    asset_task_id: str = Field(..., name='任务id', min_length=0, max_length=50)

    @validator('asset_task_id')
    def validate_asset_task_id(cls, v):
        return ObjectId(v)

    @validator('uid')
    def validate_uid(cls, v):
        if v:
            return ObjectId(v)
        return ''


class HostVulTask(BaseModel):
    uid: str = Field(None, name='用户名', min_length=0, max_length=50)
    asset_task_id: str = Field(None, name='任务id', min_length=0, max_length=50)
    name: str = Field(..., name='任务名称', min_length=1, max_length=50)
    # asset_type: str = Field(validators=[DataRequired()], name='资产类型', choices=['host', 'web'])
    trigger_type: str = Field(validators=[DataRequired()], name='任务类型', choices=['schedule', 'manual'])
    enable_monitor: bool = Field(True, name='是否开启告警')

    job_ids: List[str] = Field(..., name='job_ids', min_items=1)
    interval: int = Field(86400000, name='并发数量')
    timeout: int = Field(1, name='扫描 连接超时时间(单位: s)')
    network_scan_profile: int = Field(0, name='策略模版( 高、中、低危 默认 及全量)', choices=[0, 1, 2, 3])
    is_detect_port_enabled: bool = Field(False, name='是否探测端口')
    is_detect_os_enabled: bool = Field(False, name='是否探测操作系统')
    is_tcp_enabled: bool = Field(True, name='tcp端口探测', )
    is_udp_enabled: bool = Field(False, name='udp端口探测')

    @validator('job_ids')
    def validate_job_ids(cls, v):
        result = []
        for job_id in v:
            result.append(ObjectId(job_id))
        return result

    @validator('asset_task_id')
    def validate_asset_task_id(cls, v):
        if v:
            return ObjectId(v)
        else:
            return ''

    @validator('uid')
    def validate_uid(cls, v):
        if v:
            return ObjectId(v)
        return ''

    @validator('name')
    def validate_name(cls, value):
        if not NAME_RE.fullmatch(value):
            raise ParameterError(msg='名称格式错误，只能输入1～50个中文、英文字母、数字、下划线')
        return value


class HostVulTaskDelete(BaseModel):
    uid_list: List[str] = Field(None, name='任务ids', min_items=0)
    asset_task_ids: List[str] = Field(..., name='任务ids', min_items=1)

    @validator('asset_task_ids')
    def validate_asset_task_ids(cls, v):
        result = []
        for job_id in v:
            result.append(ObjectId(job_id))
        return result

    @validator('uid_list')
    def validate_uid_list(cls, v):
        result = set()
        for item in v:
            result.add(ObjectId(item))
        return list(result)


class HostVulTaskSwitch(BaseModel):
    uid: str = Field(None, name='用户名', min_length=0, max_length=50)
    asset_task_id: str = Field(..., name='任务id', min_length=1, max_length=50)
    enable_monitor: bool = Field(False, name='监测开关')

    @validator('asset_task_id')
    def validate_asset_task_id(cls, v):
        return ObjectId(v)

    @validator('uid')
    def validate_uid(cls, v):
        if v:
            return ObjectId(v)
        return ''


class _WsTaskTimeSetting:
    periodStartTime = StringField(default="")
    periodEndTime = StringField(default="")
    startAt = StringField(default="")

    @staticmethod
    def check_settings(time_settings, balance):
        is_support = balance.balance.wsTaskTime.total and balance.feature.wsTaskTime
        start_at = time_settings.get("startAt", "")
        start_time = time_settings.get("periodStartTime", "")
        end_time = time_settings.get("periodEndTime", "")
        ws_task_time = {
            "startAt": "",
            "periodStartTime": "",
            "periodEndTime": ""
        }
        if not any([start_at, start_time, end_time]):
            return ws_task_time
        if (not is_support) and any([start_at, start_time, end_time]):
            raise ParameterError(msg="当前套餐不支持设置执行时间")
        if start_at:
            try:
                t = arrow.get(start_at)
            except arrow.parser.ParserError:
                raise ParameterError(msg="单次任务指定时间错误")
            else:
                ws_task_time["startAt"] = t.isoformat()
        elif any([start_time, end_time]) and not all([start_time, end_time]):
            raise ParameterError(msg="周期任务指定时间范围错误, 开始与结束时间不能单独存在")
        elif all([start_time, end_time]):
            _start_time = datetime.datetime.strptime(start_time, "%H:%M:%S")
            _end_time = datetime.datetime.strptime(end_time, "%H:%M:%S")
            if _end_time < _start_time:
                _end_time = _end_time + datetime.timedelta(days=1)
            _t = (_end_time - _start_time).seconds
            if _t < 6 * 60 * 60:
                raise ParameterError(msg="时间段最小范围为6小时，请重新选择")
            elif _t > 18 * 60 * 60:
                raise ParameterError(msg="时间段最大范围为18小时，请重新选择")
            ws_task_time["periodStartTime"] = start_time
            ws_task_time["periodEndTime"] = end_time
        return ws_task_time


class WsTaskTimeSetting(_WsTaskTimeSetting, Form):
    pass


class UpdateWsTaskTime(_WsTaskTimeSetting, UidForm):
    job_ids = FieldList(StringField(validators=[DataRequired()]), min_entries=1)

    def validate_job_ids(self, ids):
        try:
            self.job_id_objs = [bson.ObjectId(item.data) for item in ids if item.data]
        except bson.errors.BSONError:
            raise ParameterError(msg="资产信息错误")


class WebSecurityTaskForm:

    name = StringField(validators=[DataRequired()])
    prelogin = MultipleFileField(default={})
    immediate_exec = BooleanField('立即执行', default=False)
    scan_speed_config = FormField(ScanSpeedConfig)
    custom_monitor_page = FormField(CustomMonitorPage)
    vul_settings = StringField(default={})
    ssl_settings = StringField(default={})
    security_event_settings = StringField(default={})
    content_settings = StringField(default={})
    change_check_settings = StringField(default={})
    ws_task_time = FormField(WsTaskTimeSetting)

    @staticmethod
    def _get_user_balance(uid=None):
        uid = g.user.id if not uid else uid
        obj = Balances.objects.filter(uid=uid).first()
        if not obj:
            raise ParameterError(msg="请开通套餐")
        return obj

    @staticmethod
    def _get_app_balance(job_list):
        uids = list({j.uid for j in job_list})
        if len(uids) > 1:
            raise ParameterError(msg="同时只能给一个用户添加任务")
        return WebSecurityTaskForm._get_user_balance(uids[0])

    def validate_name(self, value):
        value.data = value.data.strip()
        if not NAME_RE.fullmatch(value.data):
            raise CustomNameError()

    def validate_prelogin(self, value):
        try:
            trans_textbox_to_dict(value.data.get('request_http_headers', ""))
        except Exception:
            raise ParameterError(msg="headers 格式不正确")
        data = PreLogin.from_dict(value.data)
        data.validate()
        self.prelogin = data

    @staticmethod
    def update_settings(obj, job, custom_monitor_page, prelogin, manual=False):
        common_data = {
            'immediateExec': True,
            'jobId': job.id,
            'sourceIp': job.sourceIp,
            'enableMonitor': True,
            'customMonitorPage': custom_monitor_page,
        }
        if obj.change_check_settings.data and not manual:
            data = {
                'changeCheckSettings': obj.change_check_settings.data,
                'prelogin': prelogin,
                **common_data
            }
            change_check_form = ChangeCheckSettingForm(data).validate_for_api()
            if change_check_form.settings.enable:
                obj.result["change_check_count"] = obj.result["change_check_count"] + 1
            obj.result[job.id]["change_check_form"] = change_check_form
        if obj.vul_settings.data:
            data = {
                'vulSettings': obj.vul_settings.data,
                'prelogin': prelogin,
                **common_data
            }
            if not manual:
                vul_form = VulSettingsForm(data).validate_for_api()
            else:
                data.update({"taskSettings": data.pop("vulSettings")})
                vul_form = CustomVulSettingsForm(data).validate_for_api()
                _t = vul_form.settings.to_dict()
                _t.update({"interval": 0, "enable": obj.vul_settings.data.get("enable", True)})
                vul_form.settings = VulSettings.from_dict(_t)
            if vul_form.settings.enable:
                obj.result["vul_count"] = obj.result["vul_count"] + 1
            obj.result[job.id]["vul_form"] = vul_form
        if obj.ssl_settings.data and job.targetUrl.startswith("https"):
            data = {
                'sslSettings': obj.ssl_settings.data,
                **common_data
            }
            if not manual:
                ssl_form = SslSettingForm(data).validate_for_api()
            else:
                data.update({"taskSettings": data.pop("sslSettings")})
                ssl_form = CustomSslSettingForm(data).validate_for_api()
                _t = ssl_form.settings.to_dict()
                _t.update({"interval": 0, "enable": obj.ssl_settings.data.get("enable", True)})
                ssl_form.settings = SslSettings.from_dict(_t)
            if ssl_form.settings.enable:
                obj.result["ssl_count"] = obj.result["ssl_count"] + 1
            obj.result[job.id]["ssl_form"] = ssl_form
        if obj.security_event_settings.data:
            data = {
                'securityEventSettings': obj.security_event_settings.data,
                'prelogin': prelogin,
                **common_data
            }
            if not manual:
                security_event_form = SecurityEventSettingsForm(data).validate_for_api()
            else:
                data.update({"taskSettings": data.pop("securityEventSettings")})
                security_event_form = CustomSecurityEventSettingsForm(data).validate_for_api()
                _t = security_event_form.settings.to_dict()
                _t.update({"interval": 0, "enable": obj.security_event_settings.data.get("enable", True)})
                security_event_form.settings = SecurityEventSettings.from_dict(_t)
            if security_event_form.settings.enable:
                obj.result["security_event_count"] = obj.result["security_event_count"] + 1
            obj.result[job.id]["security_event_form"] = security_event_form
        if obj.content_settings.data:
            data = {
                'contentSettings': obj.content_settings.data,
                'prelogin': prelogin,
                **common_data
            }
            if not manual:
                content_form = ContentSettingsForm(data).validate_for_api()
            else:
                data.update({"taskSettings": data.pop("contentSettings")})
                content_form = CustomContentSettingsForm(data).validate_for_api()
                _t = content_form.settings.to_dict()
                _t.update({"interval": 0, "enable": obj.content_settings.data.get("enable", True)})
                content_form.settings = ContentSettings.from_dict(_t)
            if content_form.settings.enable:
                obj.result["content_count"] = obj.result["content_count"] + 1
            obj.result[job.id]["content_form"] = content_form

    @staticmethod
    def save_asset(form_obj, job, prelogin, enable_monitor: bool, trigger_type: str, affects: list):
        is_manual = True if trigger_type == "manual" else False
        item = form_obj.result.get(job.id)
        asset_task = AssetTasks()
        asset_task.name = form_obj.name.data
        asset_task.uid = job.uid
        asset_task.jobId = job.id
        asset_task.isVerified = job.verification.isVerified
        asset_task.taskGroupType = 'web_security'
        asset_task.triggerType = trigger_type
        asset_task.enableMonitor = enable_monitor
        asset_task.taskType = ''
        asset_task.targetUrl = job.targetUrl
        # 资产默认告警设置
        asset_task.alertSettings = job.alertSettings
        job.prelogin = prelogin
        asset_task.prelogin = prelogin
        job.customMonitorPage = form_obj.custom_monitor_page
        asset_task.scanSpeedConfig = form_obj.scan_speed_config
        asset_task.customMonitorPage = form_obj.custom_monitor_page
        asset_task.ws_task_time = DbWsTaskTime.from_dict(form_obj.ws_task_time)
        if vul_form := item.get("vul_form"):
            asset_task.vulSettings = vul_form.settings
        if ssl_form := item.get("ssl_form"):
            asset_task.sslSettings = ssl_form.settings
        if security_event_form := item.get("security_event_form"):
            asset_task.securityEventSettings = security_event_form.settings
        if content_form := item.get("content_form"):
            asset_task.contentSettings = content_form.settings
        if change_check_form := item.get("change_check_form"):
            asset_task.changeCheckSettings = change_check_form.settings
        is_conf_task = any([asset_task.contentSettings.enable, asset_task.securityEventSettings.enable,
                            asset_task.sslSettings.enable, asset_task.vulSettings.enable,
                            asset_task.changeCheckSettings.enable])
        is_only_ssl = asset_task.sslSettings.enable and not any([
            asset_task.contentSettings.enable,
            asset_task.securityEventSettings.enable,
            asset_task.vulSettings.enable,
            asset_task.changeCheckSettings.enable
        ])
        if (is_only_ssl and not job.targetUrl.startswith("https")) or not is_conf_task:
            return None
        asset_task.save()
        if vul_form and not is_manual:
            job.vulSettings = vul_form.settings
        if ssl_form and not is_manual:
            job.sslSettings = ssl_form.settings
        if security_event_form and not is_manual:
            job.securityEventSettings = security_event_form.settings
        if content_form and not is_manual:
            job.contentSettings = content_form.settings
        if change_check_form and not is_manual:
            job.changeCheckSettings = change_check_form.settings
        job.save()
        if is_conf_task:
            affects.append(job.targetUrl)
        return asset_task


class WebSecurityTaskAddForm(BaseForm, WebSecurityTaskForm):

    job_ids = FieldList(StringField(validators=[DataRequired()]), min_entries=1)

    def __init__(self, data=None):
        self.job_list = []
        self.asset_list = []
        self.balance = self._get_user_balance()
        self.result = {}
        super().__init__(data=data)

    def validate_job_ids(self, ids):
        if len(set(ids)) != len(ids):
            raise ParameterError(msg="资产地址重复")
        try:
            job_ids = [bson.ObjectId(item.data) for item in ids if item.data]
        except bson.errors.BSONError:
            raise ParameterError(msg="资产信息错误")
        if len(job_ids) > 50:
            raise ParameterError(msg="同时只支持50个资产创建任务")
        query = dict(id__in=job_ids, assetType__ne="host")
        if g.entity_type != "app":
            query.update({"uid": g.user.id})
        self.job_list = [o for o in Job.objects.filter(**query)]
        if len(self.job_list) != len(ids):
            raise ParameterError(msg="所选择的资产可能被删除了或者不是web资产")
        not_verified_job = [j.targetUrl for j in self.job_list if not j.verification.isVerified]
        if not_verified_job:
            raise ParameterError(msg=f"{'、'.join(not_verified_job)} 没有认证")
        asset_task = AssetTasks.objects.filter(taskGroupType="web_security", triggerType="schedule", jobId__in=job_ids)
        if asset_task:
            exists_job_id = {a.jobId: 1 for a in asset_task}
            exists_job = [j.targetUrl for j in self.job_list if exists_job_id.get(j.id)]
            raise ParameterError(msg=f"{'、'.join(exists_job)}已存在web安全监测周期任务， 只能执行修改操作")
        self.asset_list = [o.get_asset() for o in self.job_list]

    def validate_for_api(self):
        obj = super(WebSecurityTaskAddForm, self).validate_for_api()
        obj.balance = obj.balance if g.entity_type != "app" else self._get_app_balance(obj.job_list)
        obj.result.update({"vul_count": 0, "ssl_count": 0, "security_event_count": 0,
                           "content_count": 0, "change_check_count": 0})
        # 预登录和自定义页面 只支持单个资产设置周期任务
        if len(obj.job_list) > 1:
            obj.prelogin = PreLogin.from_dict({})
            obj.custom_monitor_page = Db_CustomMonitorPage.from_dict({})
        else:
            obj.prelogin = obj.prelogin
            obj.custom_monitor_page = Db_CustomMonitorPage.from_dict(obj.custom_monitor_page.data)

        new_scan_speed_config = {
            "scanSpeed": obj.scan_speed_config.data.get("scanSpeed", 0),
            "concurrency": 0,
        }
        if g.role.name == "管理员":
            new_scan_speed_config["concurrency"] = obj.scan_speed_config.data.get("concurrency", 0)
        obj.scan_speed_config = DB_ScanSpeedConfig.from_dict(dict_data=new_scan_speed_config)

        for job in obj.job_list:
            obj.result.update({job.id: {"job": job}})
            obj.update_settings(obj, job, obj.custom_monitor_page.to_dict(), obj.prelogin.to_dict())
        obj.ws_task_time = WsTaskTimeSetting.check_settings(obj.ws_task_time.data, obj.balance)
        return obj


class WebSecurityManualTaskAddForm(BaseForm, WebSecurityTaskForm):

    job_ids = FieldList(StringField(validators=[DataRequired()]), min_entries=1)

    def __init__(self, data=None):
        self.job_list = []
        self.asset_list = []
        self.balance = self._get_user_balance()
        self.result = {}
        super().__init__(data=data)

    def validate_job_ids(self, ids):
        if len(set(ids)) != len(ids):
            raise ParameterError(msg="资产地址重复")
        try:
            job_ids = [bson.ObjectId(item.data) for item in ids if item.data]
        except bson.errors.BSONError:
            raise ParameterError(msg="资产信息错误")
        if len(job_ids) > 50:
            raise ParameterError(msg="同时只支持50个资产创建任务")
        query = dict(id__in=job_ids, assetType__ne="host")
        if g.entity_type != "app":
            query.update({"uid": g.user.id})
        self.job_list = [o for o in Job.objects.filter(**query)]
        if len(self.job_list) != len(ids):
            raise ParameterError(msg="所选择的资产可能被删除了或者不是web资产")
        not_verified_job = [j.targetUrl for j in self.job_list if not j.verification.isVerified]
        if not_verified_job:
            raise ParameterError(msg=f"{'、'.join(not_verified_job)} 没有认证")
        self.asset_list = [o.get_asset() for o in self.job_list]

    def validate_for_api(self):
        obj = super(WebSecurityManualTaskAddForm, self).validate_for_api()
        obj.result.update({"vul_count": 0, "ssl_count": 0, "security_event_count": 0, "content_count": 0})
        obj.balance = obj.balance if g.entity_type != "app" else self._get_app_balance(obj.job_list)
        # 预登录和自定义页面 只支持单个资产设置任务
        obj.check_balance(obj)
        if len(obj.job_list) > 1:
            obj.prelogin = PreLogin.from_dict({})
            obj.custom_monitor_page = Db_CustomMonitorPage.from_dict({})
        else:
            obj.prelogin = obj.prelogin
            obj.custom_monitor_page = Db_CustomMonitorPage.from_dict(obj.custom_monitor_page.data)

        new_scan_speed_config = {
            "scanSpeed": obj.scan_speed_config.data.get("scanSpeed", 0),
            "concurrency": 0,
        }
        if g.role.name == "管理员":
            new_scan_speed_config["concurrency"] = obj.scan_speed_config.data.get("concurrency", 0)
        obj.scan_speed_config = DB_ScanSpeedConfig.from_dict(dict_data=new_scan_speed_config)

        for job in obj.job_list:
            obj.result.update({job.id: {"job": job}})
            obj.update_settings(obj, job, obj.custom_monitor_page.to_dict(), obj.prelogin.to_dict(), manual=True)
        err_list = []
        if obj.result["vul_count"] + obj.balance.balance.vulManualTask.used > obj.balance.balance.vulManualTask.total:
            err_list.append("Web漏洞监测超过套餐限制")
        if obj.result["ssl_count"] + obj.balance.balance.sslManualTask.used > obj.balance.balance.sslManualTask.total:
            err_list.append("Ssl监测超过套餐限制")
        if obj.result["security_event_count"] + obj.balance.balance.securityEventManualTask.used > obj.balance.balance.securityEventManualTask.total:
            err_list.append("安全事件监测超过套餐限制")
        if obj.result["content_count"] + obj.balance.balance.contentManualTask.used > obj.balance.balance.contentManualTask.total:
            err_list.append("内容违规监测超过套餐限制")
        if err_list:
            raise ParameterError(msg="、".join(err_list))
        obj.ws_task_time = WsTaskTimeSetting.check_settings(obj.ws_task_time.data, obj.balance)
        return obj

    @staticmethod
    def check_balance(obj):
        err_list = []
        if obj.ssl_settings.data.get("enable", True) and not getattr(obj.balance.feature, "sslTask", False):
            err_list.append("ssl监测")
        if obj.vul_settings.data.get("enable", True) and not getattr(obj.balance.feature, "vulTask", False):
            err_list.append("漏洞监测")
        if obj.content_settings.data.get("enable", True) and not getattr(obj.balance.feature, "contentTask", False):
            err_list.append("内容违规监测")
        if obj.security_event_settings.data.get("enable", True) and not getattr(obj.balance.feature, "securityEventTask", False):
            err_list.append("安全事件监测")
        if err_list:
            raise ParameterError(msg=f"套餐缺少{'、'.join(err_list)}服务， 请升级套餐")


class WebSecurityTaskUpdateForm(BaseForm, WebSecurityTaskForm):

    asset_task_id = StringField(validators=(DataRequired(), ))

    def validate_asset_task_id(self, value):
        uid = g.user.id
        asset_task_id = value.data
        if not asset_task_id:
            raise ParameterError(msg="参数错误")
        try:
            asset_task_id = bson.ObjectId(asset_task_id)
        except bson.errors.BSONError:
            raise ParameterError(msg="参数错误")
        query = {
            "id": asset_task_id,
            "triggerType": "schedule",
            "taskGroupType": "web_security"
        }
        if g.entity_type != "app":
            query.update({"uid": uid})
        asset_task = AssetTasks.objects.filter(**query).first()
        if not asset_task:
            raise ParameterError(msg="任务不存在")
        self.asset_task = asset_task
        self.job = Job.objects.filter(id=asset_task.jobId).first()
        self.balance = self._get_user_balance(asset_task.uid)

    def validate_for_api(self):
        obj = super(WebSecurityTaskUpdateForm, self).validate_for_api()
        obj.prelogin = obj.prelogin
        obj.custom_monitor_page = Db_CustomMonitorPage.from_dict(obj.custom_monitor_page.data)
        custom_monitor_page = obj.custom_monitor_page.to_dict()

        new_scan_speed_config = {
            "scanSpeed": obj.scan_speed_config.data.get("scanSpeed", 0),
            "concurrency": 0,
        }
        if g.role.name == "管理员":
            new_scan_speed_config["concurrency"] = obj.scan_speed_config.data.get("concurrency", 0)
        obj.scan_speed_config = DB_ScanSpeedConfig.from_dict(dict_data=new_scan_speed_config)

        prelogin = obj.prelogin.to_dict()
        common_data = {
            'immediateExec': False,
            'jobId': obj.job.id,
            'sourceIp': obj.job.sourceIp,
            'enableMonitor': obj.asset_task.enableMonitor,
            'customMonitorPage': custom_monitor_page,
        }
        if obj.change_check_settings.data:
            data = {
                'changeCheckSettings': obj.change_check_settings.data,
                'prelogin': prelogin,
                **common_data
            }
            obj.change_check_form = ChangeCheckSettingForm(data).validate_for_api()
            obj.change_check_form.settings.taskId = obj.asset_task.changeCheckSettings.taskId
        if obj.vul_settings.data:
            data = {
                'vulSettings': obj.vul_settings.data,
                'prelogin': prelogin,
                **common_data
            }
            obj.vul_form = VulSettingsForm(data).validate_for_api()
            obj.vul_form.settings.taskId = obj.asset_task.vulSettings.taskId
        if obj.ssl_settings.data and obj.job.targetUrl.startswith("https"):
            data = {
                'sslSettings': obj.ssl_settings.data,
                **common_data
            }
            obj.ssl_form = SslSettingForm(data).validate_for_api()
            obj.ssl_form.settings.taskId = obj.asset_task.sslSettings.taskId
        if obj.security_event_settings.data:
            data = {
                'securityEventSettings': obj.security_event_settings.data,
                'prelogin': prelogin,
                **common_data
            }
            obj.security_event_form = SecurityEventSettingsForm(data).validate_for_api()
            obj.security_event_form.settings.taskId = obj.asset_task.securityEventSettings.taskId
        if obj.content_settings.data:
            data = {
                'contentSettings': obj.content_settings.data,
                'prelogin': prelogin,
                **common_data
            }
            obj.content_form = ContentSettingsForm(data).validate_for_api()
            obj.content_form.settings.taskId = obj.asset_task.contentSettings.taskId
        obj.ws_task_time = WsTaskTimeSetting.check_settings(obj.ws_task_time.data, obj.balance)
        return obj


class WebSecurityBatchForm(BaseForm):
    task_group_type_query = {"taskGroupType": "web_security"}
    trigger_type_query = {}
    asset_task_ids = FieldList(StringField(validators=[DataRequired()]), min_entries=1)

    def validate_asset_task_ids(self, ids):
        try:
            asset_task_ids = [bson.ObjectId(item.data) for item in ids if item.data]
        except bson.errors.BSONError:
            raise ParameterError(msg="任务Id格式错误")
        query = {
            "id__in": asset_task_ids
        }
        query.update(self.task_group_type_query)
        query.update(self.trigger_type_query)
        if g.entity_type != "app":
            query.update({"uid": g.user.id})
        self.asset_tasks = [a for a in AssetTasks.objects.filter(**query)]
        if not self.asset_tasks:
            raise ParameterError(msg="所选任务都不存在")
        self.job_map = {j.id: j for j in Job.objects.filter(id__in=[a.jobId for a in self.asset_tasks])}


class WebSecurityBatchSwitchForm(WebSecurityBatchForm):
    trigger_type_query = {"triggerType": "schedule"}
    enable = BooleanField(default=True)

    def validate_for_api(self):
        obj = super(WebSecurityBatchSwitchForm, self).validate_for_api()
        if not obj.enable.data:
            return obj
        now = datetime.datetime.utcnow()
        if g.entity_type == "app":
            uids = [a.uid for a in obj.asset_tasks]
            user_map = {u.id: u.username for u in User.objects.filter(id__in=uids)}
            username_list = []
            for b in Balances.objects.filter(uid__in=uids):
                if (not b.planExpireTime) or b.planExpireTime < now:
                    username_list.append(user_map.get(b.uid))
            if username_list:
                raise ParameterError(msg=f"{'、'.join(username_list)} 套餐过期或者是免费套餐，不能开启监测")
        else:
            exp_time = WebSecurityTaskForm._get_user_balance(g.user.id).planExpireTime
            if (not exp_time) or exp_time < now:
                raise ParameterError(msg="套餐过期或者是免费套餐，不能开启监测")
        return obj


class WebSecurityBatchStopForm(WebSecurityBatchForm):
    trigger_type_query = {"triggerType": "manual"}


class AvailableTaskAddForm(BaseForm):

    immediate_exec = BooleanField('立即执行', default=True)
    task_type = StringField(validators=[DataRequired()])
    task_settings = FieldList(StringField(), default=[])
    job_ids = FieldList(StringField(validators=[DataRequired()]), min_entries=1)

    def validate_task_type(self, value):
        if value.data not in ["http", "ping"]:
            raise ParameterError(msg="任务类型错误")

    def validate_job_ids(self, ids):
        if len(set(ids)) != len(ids):
            raise ParameterError(msg="资产地址重复")
        try:
            job_ids = [bson.ObjectId(item.data) for item in ids if item.data]
        except bson.errors.BSONError:
            raise ParameterError(msg="资产信息错误")
        self.job_ids = job_ids

    def validate_task_settings(self, value):
        if not value.data:
            raise ParameterError(msg="没有任何配置")
        if len(value.data) > 350:
            raise ParameterError(msg="最多支持同时创建350个任务")

    def validate_for_api(self):
        obj = super(AvailableTaskAddForm, self).validate_for_api()
        task_type = obj.task_type.data
        form = HttpSettingsForm if task_type == TaskType.http.value else PingSettingsForm
        obj.job_list = obj.check_job(task_type, obj.job_ids)
        job_form_map = {}
        setting_map = obj.check_settings(obj.job_list, obj.task_settings.data, task_type)
        obj.balance = WebSecurityTaskForm._get_user_balance() if g.entity_type != "app" else \
            WebSecurityTaskForm._get_app_balance(obj.job_list)
        count = 0
        for job in obj.job_list:
            setting_list = setting_map.get(str(job.id))
            data = {
                "jobId": str(job.id),
                "enableMonitor": True,
                "immediateExec": True,
                f"{task_type}Settings": setting_list
            }
            job_form_map.update({job.id: form(data).validate_for_api()})
            count += len(setting_list)
        obj.job_form_map = job_form_map
        obj.count = count
        if task_type == "http" and count + obj.balance.balance.httpMaxTask.used > obj.balance.balance.httpMaxTask.total:
            raise ParameterError(msg="http监测任务超过套餐限制")
        elif task_type == "ping" and count + obj.balance.balance.pingMaxTask.used > obj.balance.balance.pingMaxTask.total:
            raise ParameterError(msg="ping监测任务超过套餐限制")
        return obj

    @staticmethod
    def check_job(task_type, job_ids):
        # 主机监测没有上线， 暂时ping任务也去掉主机资产
        query = dict(id__in=job_ids, assetType__ne="host")
        if g.entity_type != "app":
            query.update({"uid": g.user.id})
        job_list = [o for o in Job.objects.filter(**query)]
        if len(job_list) != len(job_ids):
            raise ParameterError(msg="所选择的资产可能被删除了")
        not_verified_job = [j.targetUrl for j in job_list if not j.verification.isVerified]
        if not_verified_job:
            raise ParameterError(msg=f"{'、'.join(not_verified_job)} 没有认证")
        return job_list

    @staticmethod
    def check_status_code_range(status_code_range):
        if not isinstance(status_code_range, list):
            raise ParameterError(msg="状态码范围错误")
        for code_range in status_code_range:
            if not isinstance(code_range, list):
                raise ParameterError(msg="状态码范围错误")
            for code in code_range:
                try:
                    code = int(code)
                except (ValueError, TypeError):
                    raise ParameterError(msg="状态码范围错误")

    @staticmethod
    def check_setting_args(setting):
        if not isinstance(setting, dict):
            raise ParameterError(msg="参数格式错误")
        if status_code_range := setting.get("alert", {}).get("statusCodeRange", []):
            AvailableTaskAddForm.check_status_code_range(status_code_range)
        if not NAME_RE.fullmatch(setting.get("name", "")):
            raise CustomNameError(msg=f"名称 {setting.get('name', '')} 格式错误，只能输入1～50个中文、英文字母、数字、下划线")

    @staticmethod
    def check_settings(job_list, settings, task_type):
        job_ip_map = {str(j.id): j.sourceIp for j in job_list}
        asset_tasks = AssetTasks.objects.find({"triggerType": "schedule", "taskGroupType": task_type, "jobId": {"$in": [j.id for j in job_list]}})
        asset_task_exists = {}
        for a in asset_tasks:
            if not asset_task_exists.get(str(a.jobId)):
                asset_task_exists.update({str(a.jobId): {getattr(a, f"{task_type}Settings").to_dict().get("target"): 1}})
            else:
                asset_task_exists[str(a.jobId)].update({getattr(a, f"{task_type}Settings").to_dict().get("target"): 1})
        setting_map = {}
        exists_url = {}
        error_msg = []
        repeat_url = set()
        old_url = set()
        for setting in settings:
            if not isinstance(setting, dict):
                continue
            AvailableTaskAddForm.check_setting_args(setting)
            if not (target := setting.get("target", "")):
                continue
            if not (job_id := setting.pop("job_id", "")):
                continue
            ip_target = f"{target}({job_ip_map.get(job_id)})" if job_ip_map.get(job_id) else f"{target}"
            if exists_url.get(ip_target):
                repeat_url.add(ip_target)
                continue
            if asset_task_exists.get(job_id, {}).get(target):
                old_url.add(ip_target)
                continue
            if not setting_map.get(job_id):
                setting_map[job_id] = [setting]
            else:
                setting_map[job_id].append(setting)
            exists_url.update({ip_target: 1})
        if repeat_url:
            error_msg.extend([f"{u}重复" for u in repeat_url])
        if old_url:
            error_msg.extend([f"{u}历史配置中已经存在" for u in old_url])
        if error_msg:
            raise ParameterError(msg=",".join(error_msg))
        if not setting_map:
            raise ParameterError(msg="没有任务下发")
        return setting_map

    @staticmethod
    def save_asset_task(setting, job, enable_monitor: bool, trigger_type: str, task_type: str):
        asset_task = AssetTasks()
        # 可用性任务名字存在settings中
        asset_task.name = setting.name
        asset_task.uid = job.uid
        asset_task.jobId = job.id
        asset_task.isVerified = job.verification.isVerified
        asset_task.taskGroupType = task_type
        asset_task.triggerType = trigger_type
        asset_task.enableMonitor = enable_monitor
        asset_task.targetUrl = job.targetUrl
        asset_task.taskType = task_type
        # 资产默认告警设置
        asset_task.alertSettings = job.alertSettings
        if task_type == "http":
            asset_task.httpSettings = setting
        else:
            asset_task.pingSettings = setting
        asset_task.save()
        return asset_task


class AvailableTaskUpdateForm(BaseForm):

    asset_task_id = StringField(validators=(DataRequired(), ))
    task_setting = StringField(validators=(DataRequired(), ))

    def validate_task_setting(self, value):
        if data := value.data:
            AvailableTaskAddForm.check_setting_args(data)

    def validate_asset_task_id(self, value):
        uid = g.user.id
        asset_task_id = value.data
        if not asset_task_id:
            raise ParameterError(msg="参数错误")
        try:
            asset_task_id = bson.ObjectId(asset_task_id)
        except bson.errors.BSONError:
            raise ParameterError(msg="参数错误")
        query = {
            "id": asset_task_id,
            "triggerType": "schedule",
            "taskGroupType__in": ["http", "ping"]
        }
        if g.entity_type != "app":
            query.update({"uid": uid})
        asset_task = AssetTasks.objects.filter(**query).first()
        if not asset_task:
            raise ParameterError(msg="任务不存在")
        self.asset_task = asset_task

    def validate_for_api(self):
        obj = super(AvailableTaskUpdateForm, self).validate_for_api()
        asset_task = obj.asset_task
        form = HttpSettingsForm if asset_task.taskGroupType == "http" else PingSettingsForm
        data = {
            "jobId": str(asset_task.jobId),
            "enableMonitor": True,
            "immediateExec": True
        }
        if asset_task.taskGroupType == "http":
            obj.task_setting.data.update({"taskId": asset_task.httpSettings.taskId})
            data.update({"httpSettings": [obj.task_setting.data]})
        else:
            obj.task_setting.data.update({"taskId": asset_task.pingSettings.taskId})
            data.update({"pingSettings": [obj.task_setting.data]})
        form_obj = form(data).validate_for_api()
        obj.setting = form_obj.settings[0]
        obj.job = form_obj.job
        return obj


class AvailableBatchForm(WebSecurityBatchForm):
    task_group_type_query = {"taskGroupType__in": ["http", "ping"]}


class AvailableBatchSwitchForm(WebSecurityBatchSwitchForm):
    task_group_type_query = {"taskGroupType__in": ["http", "ping"]}


class UpdateTaskGroupConfigForm(BaseForm):
    task_type = StringField(default="all", )
    usernames = FieldList(StringField())

    def validate_task_type(self, value):
        if value.data not in ["all", 'host', 'web_security', 'http', 'ping', 'web_asset']:
            raise ParameterError(msg="任务类型错误")

    def validate_for_api(self):
        obj = super(UpdateTaskGroupConfigForm, self).validate_for_api()
        now = datetime.datetime.utcnow()
        target_uids = []
        uids = []
        if usernames := obj.usernames.data:
            target_uids.extend([u.id for u in User.objects.filter(username__in=usernames)])
        if usernames and not target_uids:
            raise ParameterError(msg=f"用户{usernames} 不存在")
        balances = Balances.objects.all() if not target_uids else Balances.objects.filter(uid__in=target_uids)
        for b in balances:
            if b.planExpireTime and  b.planExpireTime < now:
                uids.append(b.uid)
        if not uids:
            raise ParameterError(msg=f"用户服务都已经过期，不能进行配置更新")
        if obj.task_type.data == "all":
            obj.asset_tasks = AssetTasks.objects.filter(taskGroupType__in=['host', 'web_security', 'http', 'ping', 'web_asset'],
                                                        triggerType="schedule",
                                                        uid__in=uids)
        else:
            obj.asset_tasks = AssetTasks.objects.filter(taskGroupType=obj.task_type.data,
                                                        triggerType="schedule",
                                                        uid__in=uids)
        return obj


class EnableProxyIpForm(BaseForm):

    enable_proxy_ip = BooleanField()
    job_ids = FieldList(StringField())

    def validate_job_ids(self, ids):
        if len(set(ids)) != len(ids):
            raise ParameterError(msg="资产地址重复")
        try:
            job_ids = [bson.ObjectId(item.data) for item in ids if item.data]
        except bson.errors.BSONError:
            raise ParameterError(msg="资产信息错误")
        self.job_ids = job_ids
